import abc
import collections
import datetime
import logging
import tabulate

from coin.base.param_util import to_list
from coin.proto.coin_query_pb2 import CurrencyBalance
from coin.strategy.accounting.pnl_balance_printer.util import get_quote_currency


class PnlBalancePrinter(object):
  __metaclass__ = abc.ABCMeta

  def __init__(self, products, bookmap, logger=None):
    self._logger = logger or logging.getLogger('PnlBalancePrinter')
    products = to_list(products)
    assert isinstance(products, list) and len(products) > 0
    self._products = list(set(products))
    self._bookmap = bookmap

    self._og = None
    self.started = False

  def register_order_gateway(self, order_gateway):
    self._og = order_gateway

  def calculate_pnl_balance(self):
    if self._og is None or not self._og.is_ready():
      return None
    else:
      return self.calculate_pnl_balance_impl()

  @abc.abstractmethod
  def print_pnl_balance(self):
    raise NotImplementedError()

  @abc.abstractmethod
  def calculate_pnl_balance_impl(self):
    raise NotImplementedError()


class SpotPnlBalancePrinter(PnlBalancePrinter):
  def __init__(self, products, bookmap, reserve_map, logger=None):
    logger = logger or logging.getLogger('SpotBalancePrinter')
    super().__init__(products, bookmap, logger)
    self._reserve_map = reserve_map

    currencies = []
    quote_currency_str = None
    for product in self._products:
      assert product.base.currency in reserve_map, product
      if quote_currency_str is None:
        quote_currency_str = get_quote_currency(product).currency
      else:
        assert quote_currency_str == get_quote_currency(product).currency
      currencies.append(product.base.currency)
    currencies = list(set(currencies))

    self._quote_currency_str = quote_currency_str

    self._og = None
    self.started = False

  def _get_reserve_balance_price(self, product):
    reserve = self._reserve_map.get(product.base.currency, None)
    balance = self._og.get_balance(product.base.currency)

    book = self._bookmap.get_book(product)
    if book is None or not book.has_ask() or not book.has_bid():
      mtm_price = None
    else:
      mtm_price = 0.5 * (book.ask0().price + book.bid0().price)
    return reserve, balance, mtm_price

  def calculate_pnl_balance_impl(self):
    pnl_balances = []
    pnlbal_total = 0.
    for product in self._products:
      reserve, balance, price = self._get_reserve_balance_price(product)
      pnlbal_total += (balance - reserve) * price
    pnlbal_total += self._og.get_balance(self._quote_currency_str)
    pnl_balance = CurrencyBalance(currency=self._quote_currency_str, total=pnlbal_total)
    pnl_balances.append(pnl_balance)
    return pnl_balances

  def print_pnl_balance(self, ignore_error=False):
    try:
      if self._og is None or not self._og.is_ready():
        return

      table = []
      total = 0.
      pnlbal_total = 0.
      for product in self._products:
        reserve, balance, price = self._get_reserve_balance_price(product)
        table.append([
            '*',
            product.base.currency,
            reserve,
            balance,
            price,
            balance * price, (balance - reserve) * price
        ])
        total += balance * price
        pnlbal_total += (balance - reserve) * price

      balance = self._og.get_balance(self._quote_currency_str)
      total += balance
      pnlbal_total += balance

      table.append(['*', self._quote_currency_str, None, balance, 1, balance, balance])
      table.append(['*', 'Total', None, None, None, total, pnlbal_total])

      pos_bal_table_str = tabulate.tabulate(table,
                                            headers=[
                                                '*',
                                                'Currency',
                                                'Reserve',
                                                'Balance',
                                                'QuotePrice',
                                                'BalanceInQuote',
                                                'PnlBalanceInQuote'
                                            ],
                                            floatfmt='.4f')
      self._logger.info('----- Reserve and balance:\n* %s\n%s',
                        str(datetime.datetime.now()),
                        pos_bal_table_str)
    except Exception:
      if ignore_error:
        self._logger.exception('Exception raised')
      else:
        raise


class FuturesPnlBalancePrinter(PnlBalancePrinter):
  def __init__(self, products, bookmap, reserve_map, logger=None):
    logger = logger or logging.getLogger('FuturesBalancePrinter')
    super().__init__(products, bookmap, logger)

  def calculate_pnl_balance_impl(self):
    pnl_balances = []
    pos_pnl_dict = collections.defaultdict(list)
    for product in self._products:
      quote_str = get_quote_currency(product).currency
      pos, unrealized_pnl = self._get_pos(product)
      pos_pnl_dict[quote_str].append({
          'product': product, 'pos': pos, 'unrealized_pnl': unrealized_pnl
      })
    for quote_str, pos_pnls in pos_pnl_dict.items():
      total = 0.
      for pos_pnl in pos_pnls:
        unrealized_pnl = pos_pnl['unrealized_pnl']
        total += (unrealized_pnl or 0.)
      balance = self.get_balance(quote_str)
      total += balance
      pnl_balance = CurrencyBalance(currency=quote_str, total=total)
      pnl_balances.append(pnl_balance)
    return pnl_balances

  def get_balance(self, currency):
    return self._og.get_balance(currency)

  def print_pnl_balance(self, ignore_error=False):
    try:
      if self._og is None or not self._og.is_ready():
        return

      table = []
      pos_pnl_dict = collections.defaultdict(list)

      for product in self._products:
        quote_str = get_quote_currency(product).currency
        pos, unrealized_pnl = self._get_pos(product)
        pos_pnl_dict[quote_str].append({
            'product': product, 'pos': pos, 'unrealized_pnl': unrealized_pnl
        })

      for quote_str, pos_pnls in pos_pnl_dict.items():
        total = 0.
        for pos_pnl in pos_pnls:
          symbol = pos_pnl['product'].symbol
          pos = pos_pnl['pos']
          unrealized_pnl = pos_pnl['unrealized_pnl']
          total += (unrealized_pnl or 0.)
          table.append(['*', symbol, pos, unrealized_pnl])
        balance = self.get_balance(quote_str)
        total += balance
        table.append(['*', quote_str, None, balance])
        table.append(['*', 'Total%s' % quote_str, None, total])

      pos_bal_table_str = tabulate.tabulate(table,
                                            headers=['*', 'Product', 'Position', 'Value'],
                                            floatfmt='.4f')
      self._logger.info('----- Position and balance:\n* %s\n%s',
                        str(datetime.datetime.now()),
                        pos_bal_table_str)
    except Exception:
      if ignore_error:
        self._logger.exception('Exception raised')
      else:
        raise

  def _get_pos(self, product):
    raise NotImplementedError
